﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using MentalAlchemy.Atomics;

namespace MentalAlchemy.Molecules.MachineLearning
{
	/// <summary>
	/// Class for Generalized Hebbian Algorithm.
	/// </summary>
	public class GHA
	{
		public static float TrainingRate = 0.01f;
		public static int DataAmount = 100;

		/// <summary>
		/// [molecule]
		/// 
		/// Updates weights of the given network.
		/// </summary>
		/// <param name="net"></param>
		/// <param name="data"></param>
		public virtual void UpdateWeights(ref LayeredNeuralNetwork net, List<TrainingSample> data)
		{
			var dataCount = data.Count;
			var outputsCount = net.OutputsNumber;
			var inputsCount = net.InputsNumber;
			for (int k = 0; k < dataCount; k++)
			{
				// compute ANN response.
				var ins = MatrixMath.ConvertToVector(data[k].Data);
				net.Calculate(ins);

				float[] outs;
				net.GetOutputs(out outs);

				for (int j = 0; j < outputsCount; j++)
				{
					for (int i = 0; i < inputsCount; i++)
					{
						#region - This can be made faster using temporal array to accumulate factors for each output. -
						var temp = 0f;
						for (int k1 = 0; k1 <= j; k1++)
						{
							var node = net.Layers[1].Nodes[k1];
							temp += node.Weights[i] * outs[k1];
						}
						#endregion
						//if (float.IsNaN(temp)) {throw new Exception("temp is NaN");}
						var delta = TrainingRate * (outs[j] * ins[i] - outs[j] * temp);
						//if (float.IsNaN(delta)) { throw new Exception("delta is NaN"); }
						net.Layers[1].Nodes[j].Weights[i] += delta;
					}
					net.Layers[1].Nodes[j].NormalizeWeights();	// to prevent uncontrollable growth of weights.
				}
			}
			// todo: rewrite this in a matrix form (http://en.wikipedia.org/wiki/Generalized_Hebbian_Algorithm).
			// todo: \delta w = \etha * (y x^T - LT [y y^T] w).
		}

		/// <summary>
		/// [molecule]
		/// 
		/// Updates weights of the given network using only part of the training data (defined by the [DataAmount] variable).
		/// </summary>
		/// <param name="net"></param>
		/// <param name="data"></param>
		/// <param name="dataAmount">Amount of data to be used (in %).</param>
		public virtual void UpdateWeights(ref LayeredNeuralNetwork net, List<TrainingSample> data, int dataAmount)
		{
			var dataCount = data.Count * dataAmount / 100;

			// sample given data set.
			var sData = MachineLearningElements.SampleRandom(data, dataCount);

			var outputsCount = net.OutputsNumber;
			var inputsCount = net.InputsNumber;
			for (int k = 0; k < dataCount; k++)
			{
				// compute ANN response.
				var ins = MatrixMath.ConvertToVector(sData[k].Data);
				net.Calculate(ins);

				float[] outs;
				net.GetOutputs(out outs);

				for (int j = 0; j < outputsCount; j++)
				{
					for (int i = 0; i < inputsCount; i++)
					{
						#region - This can be made faster using temporal array to accumulate factors for each output. -
						var temp = 0f;
						for (int k1 = 0; k1 <= j; k1++)
						{
							var node = net.Layers[1].Nodes[k1];
							temp += node.Weights[i] * outs[k1];
						}
						#endregion
						//if (float.IsNaN(temp)) {throw new Exception("temp is NaN");}
						var delta = TrainingRate * (outs[j] * ins[i] - outs[j] * temp);
						//if (float.IsNaN(delta)) { throw new Exception("delta is NaN"); }
						net.Layers[1].Nodes[j].Weights[i] += delta;
					}
					net.Layers[1].Nodes[j].NormalizeWeights();	// to prevent uncontrollable growth of weights.
				}
			}
			// todo: rewrite this in a matrix form (http://en.wikipedia.org/wiki/Generalized_Hebbian_Algorithm).
			// todo: \delta w = \etha * (y x^T - LT [y y^T] w).
		}

		/// <summary>
		/// [molecule]
		/// 
		/// Performs training of ANN using given data for a specified number of epochs.
		/// </summary>
		/// <param name="net"></param>
		/// <param name="data"></param>
		/// <param name="steps"></param>
		public virtual LayeredNeuralNetwork Train(LayeredNeuralNetwork net, List<TrainingSample> data, int steps, out GhaResults results)
		{
			results = new GhaResults();

			var epochStep = 5;
			var timer = new Stopwatch();
			timer.Start();
			for (int i = 0; i < steps; i++)
			{
				timer.Stop();
				#region - Log weights. -
				if (i % epochStep == 0)
				{	// remember weights after each [epochStep] generations.
					results.Weights.Add(net.GetConnectionWeights().ToArray());
				}
				#endregion

				#region - Compute output variances. -
				var outs = MachineLearningElements.GetOutputs(net, data);
				var vars = VectorMath.VarianceVector(outs);
				results.OutputVars.Add(vars);
				#endregion
				timer.Start();

				TrainingRate = 1f / (i + 1);
				if (DataAmount == 100)
				{	// use entire set training samples.
					UpdateWeights(ref net, data);
				}
				else
				{
					UpdateWeights(ref net, data, DataAmount);
				}
			}
			timer.Stop();

			results.ElapsedTime = timer.ElapsedMilliseconds;
			results.Weights.Add(net.GetConnectionWeights().ToArray());	// remember resulting weights.
			results.EpochStep = epochStep;
			results.Outputs = net.OutputsNumber;

			return net;
		}
	}

	/// <summary>
	/// Class to represent GHA results.
	/// </summary>
	public class GhaResults
	{
		public List<float[]> OutputVars = new List<float[]>();	// Variances for outputs.
		public List<float[]> Weights = new List<float[]>();	// Weights.
		public int EpochStep = 5;	// Number of epochs between weights logging.
		public int Outputs = 0;	// Number of ANN outputs.
		public long ElapsedTime;	// Elapsed time in msec.

		/// <summary>
		/// [molecule]
		/// 
		/// Returns lines for log-file using data from the given GHA results.
		/// </summary>
		/// <returns></returns>
		public virtual List<string> ConvertToLogLines ()
		{
			var res = new List<string>();

			// write elapsed time.
			res.Add("> Elapsed time, msec:\t" + ElapsedTime);

			// write variances.
			res.Add("\n> Outputs variances:");
			var count = 0;
			foreach (var outputVar in OutputVars)
			{
				res.Add(count + "\t" + VectorMath.ConvertToString(outputVar, '\t'));
				count++;
			}

			// write weights.
			res.Add("\n> ANN weights:");
			int epochCount = 0;
			foreach (var ws in Weights)
			{
				res.Add("\nEpoch #" + epochCount);
				//var w = VectorMath.Split(ws, (int)Math.Sqrt(ws.Length));
				var w = VectorMath.Split(ws, Outputs);
				var m = MatrixMath.CreateFromColsList(w);
				res.AddRange(MatrixMath.ConvertToRowsStringsList(m, '\t'));
				epochCount += EpochStep;
			}

			return res;
		}
	}

	/// <summary>
	/// [molecule]
	/// 
	/// Class for mean results of multiple GHA runs.
	/// </summary>
	public class MeanGhaResults : GhaResults
	{
		public Stats TimeStats;	// Stats for time.

		/// <summary>
		/// [molecule]
		/// 
		/// Compute mean GHA results from the given list of individual run results.
		/// Note: Each run should be performed with the same settings.
		/// </summary>
		/// <param name="res"></param>
		/// <returns></returns>
		public static MeanGhaResults Compute (List<GhaResults> res)
		{
			var mean = new MeanGhaResults();

			var times = new List<int>();
			foreach (var ghaRes in res)
			{
				MatrixMath.Accumulate(ref mean.OutputVars, ghaRes.OutputVars);
				MatrixMath.AccumulateSigned(ref mean.Weights, ghaRes.Weights);
				times.Add((int)ghaRes.ElapsedTime);
			}

			var tempM = MatrixMath.CreateFromRowsList(mean.OutputVars);
			tempM = MatrixMath.Mul(tempM, 1f / res.Count);
			mean.OutputVars = MatrixMath.ConvertToRowsList(tempM);	// compute mean vars.
			tempM = MatrixMath.CreateFromRowsList(mean.Weights);
			tempM = MatrixMath.Mul(tempM, 1f / res.Count);
			mean.Weights = MatrixMath.ConvertToRowsList(tempM);	// compute mean weights.

			mean.TimeStats = VectorMath.CalculateStats(times.ToArray());
			mean.EpochStep = res[0].EpochStep;

			return mean;
		}

		/// <summary>
		/// [molecule]
		/// 
		/// Returns lines for log-file using data from the given GHA results.
		/// </summary>
		/// <returns></returns>
		public override List<string> ConvertToLogLines()
		{
			var res = new List<string>();

			// write time stats.
			res.Add("> Elapsed time stats, msec:");
			res.Add(TimeStats.GetStatsHeader());
			res.Add(TimeStats.GetStatsString());

			// write variances.
			res.Add("\n> Outputs variances:");
			var count = 0;
			foreach (var outputVar in OutputVars)
			{
				res.Add(count + "\t" + VectorMath.ConvertToString(outputVar, '\t'));
				count++;
			}

			// write weights.
			res.Add("\n> ANN weights:");
			int epochCount = 0;
			foreach (var ws in Weights)
			{
				res.Add("\nEpoch #" + epochCount);
				var w = VectorMath.Split(ws, (int)Math.Sqrt(ws.Length));
				var m = MatrixMath.CreateFromColsList(w);
				res.AddRange(MatrixMath.ConvertToRowsStringsList(m, '\t'));
				epochCount += EpochStep;
			}

			return res;
		}
	}
}
